// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(
      require('../../lib/codemirror'),
      require('../htmlmixed/htmlmixed'),
      require('../../addon/mode/overlay'),
    );
  else if (typeof define == 'function' && define.amd)
    // AMD
    define([
      '../../lib/codemirror',
      '../htmlmixed/htmlmixed',
      '../../addon/mode/overlay',
    ], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  CodeMirror.defineMode('django:inner', function() {
    var keywords = [
        'block',
        'endblock',
        'for',
        'endfor',
        'true',
        'false',
        'filter',
        'endfilter',
        'loop',
        'none',
        'self',
        'super',
        'if',
        'elif',
        'endif',
        'as',
        'else',
        'import',
        'with',
        'endwith',
        'without',
        'context',
        'ifequal',
        'endifequal',
        'ifnotequal',
        'endifnotequal',
        'extends',
        'include',
        'load',
        'comment',
        'endcomment',
        'empty',
        'url',
        'static',
        'trans',
        'blocktrans',
        'endblocktrans',
        'now',
        'regroup',
        'lorem',
        'ifchanged',
        'endifchanged',
        'firstof',
        'debug',
        'cycle',
        'csrf_token',
        'autoescape',
        'endautoescape',
        'spaceless',
        'endspaceless',
        'ssi',
        'templatetag',
        'verbatim',
        'endverbatim',
        'widthratio',
      ],
      filters = [
        'add',
        'addslashes',
        'capfirst',
        'center',
        'cut',
        'date',
        'default',
        'default_if_none',
        'dictsort',
        'dictsortreversed',
        'divisibleby',
        'escape',
        'escapejs',
        'filesizeformat',
        'first',
        'floatformat',
        'force_escape',
        'get_digit',
        'iriencode',
        'join',
        'last',
        'length',
        'length_is',
        'linebreaks',
        'linebreaksbr',
        'linenumbers',
        'ljust',
        'lower',
        'make_list',
        'phone2numeric',
        'pluralize',
        'pprint',
        'random',
        'removetags',
        'rjust',
        'safe',
        'safeseq',
        'slice',
        'slugify',
        'stringformat',
        'striptags',
        'time',
        'timesince',
        'timeuntil',
        'title',
        'truncatechars',
        'truncatechars_html',
        'truncatewords',
        'truncatewords_html',
        'unordered_list',
        'upper',
        'urlencode',
        'urlize',
        'urlizetrunc',
        'wordcount',
        'wordwrap',
        'yesno',
      ],
      operators = ['==', '!=', '<', '>', '<=', '>='],
      wordOperators = ['in', 'not', 'or', 'and'];

    keywords = new RegExp('^\\b(' + keywords.join('|') + ')\\b');
    filters = new RegExp('^\\b(' + filters.join('|') + ')\\b');
    operators = new RegExp('^\\b(' + operators.join('|') + ')\\b');
    wordOperators = new RegExp('^\\b(' + wordOperators.join('|') + ')\\b');

    // We have to return "null" instead of null, in order to avoid string
    // styling as the default, when using Django templates inside HTML
    // element attributes
    function tokenBase(stream, state) {
      // Attempt to identify a variable, template or comment tag respectively
      if (stream.match('{{')) {
        state.tokenize = inVariable;
        return 'tag';
      } else if (stream.match('{%')) {
        state.tokenize = inTag;
        return 'tag';
      } else if (stream.match('{#')) {
        state.tokenize = inComment;
        return 'comment';
      }

      // Ignore completely any stream series that do not match the
      // Django template opening tags.
      while (stream.next() != null && !stream.match(/\{[{%#]/, false)) {}
      return null;
    }

    // A string can be included in either single or double quotes (this is
    // the delimiter). Mark everything as a string until the start delimiter
    // occurs again.
    function inString(delimiter, previousTokenizer) {
      return function(stream, state) {
        if (!state.escapeNext && stream.eat(delimiter)) {
          state.tokenize = previousTokenizer;
        } else {
          if (state.escapeNext) {
            state.escapeNext = false;
          }

          var ch = stream.next();

          // Take into account the backslash for escaping characters, such as
          // the string delimiter.
          if (ch == '\\') {
            state.escapeNext = true;
          }
        }

        return 'string';
      };
    }

    // Apply Django template variable syntax highlighting
    function inVariable(stream, state) {
      // Attempt to match a dot that precedes a property
      if (state.waitDot) {
        state.waitDot = false;

        if (stream.peek() != '.') {
          return 'null';
        }

        // Dot followed by a non-word character should be considered an error.
        if (stream.match(/\.\W+/)) {
          return 'error';
        } else if (stream.eat('.')) {
          state.waitProperty = true;
          return 'null';
        } else {
          throw Error('Unexpected error while waiting for property.');
        }
      }

      // Attempt to match a pipe that precedes a filter
      if (state.waitPipe) {
        state.waitPipe = false;

        if (stream.peek() != '|') {
          return 'null';
        }

        // Pipe followed by a non-word character should be considered an error.
        if (stream.match(/\.\W+/)) {
          return 'error';
        } else if (stream.eat('|')) {
          state.waitFilter = true;
          return 'null';
        } else {
          throw Error('Unexpected error while waiting for filter.');
        }
      }

      // Highlight properties
      if (state.waitProperty) {
        state.waitProperty = false;
        if (stream.match(/\b(\w+)\b/)) {
          state.waitDot = true; // A property can be followed by another property
          state.waitPipe = true; // A property can be followed by a filter
          return 'property';
        }
      }

      // Highlight filters
      if (state.waitFilter) {
        state.waitFilter = false;
        if (stream.match(filters)) {
          return 'variable-2';
        }
      }

      // Ignore all white spaces
      if (stream.eatSpace()) {
        state.waitProperty = false;
        return 'null';
      }

      // Identify numbers
      if (stream.match(/\b\d+(\.\d+)?\b/)) {
        return 'number';
      }

      // Identify strings
      if (stream.match("'")) {
        state.tokenize = inString("'", state.tokenize);
        return 'string';
      } else if (stream.match('"')) {
        state.tokenize = inString('"', state.tokenize);
        return 'string';
      }

      // Attempt to find the variable
      if (stream.match(/\b(\w+)\b/) && !state.foundVariable) {
        state.waitDot = true;
        state.waitPipe = true; // A property can be followed by a filter
        return 'variable';
      }

      // If found closing tag reset
      if (stream.match('}}')) {
        state.waitProperty = null;
        state.waitFilter = null;
        state.waitDot = null;
        state.waitPipe = null;
        state.tokenize = tokenBase;
        return 'tag';
      }

      // If nothing was found, advance to the next character
      stream.next();
      return 'null';
    }

    function inTag(stream, state) {
      // Attempt to match a dot that precedes a property
      if (state.waitDot) {
        state.waitDot = false;

        if (stream.peek() != '.') {
          return 'null';
        }

        // Dot followed by a non-word character should be considered an error.
        if (stream.match(/\.\W+/)) {
          return 'error';
        } else if (stream.eat('.')) {
          state.waitProperty = true;
          return 'null';
        } else {
          throw Error('Unexpected error while waiting for property.');
        }
      }

      // Attempt to match a pipe that precedes a filter
      if (state.waitPipe) {
        state.waitPipe = false;

        if (stream.peek() != '|') {
          return 'null';
        }

        // Pipe followed by a non-word character should be considered an error.
        if (stream.match(/\.\W+/)) {
          return 'error';
        } else if (stream.eat('|')) {
          state.waitFilter = true;
          return 'null';
        } else {
          throw Error('Unexpected error while waiting for filter.');
        }
      }

      // Highlight properties
      if (state.waitProperty) {
        state.waitProperty = false;
        if (stream.match(/\b(\w+)\b/)) {
          state.waitDot = true; // A property can be followed by another property
          state.waitPipe = true; // A property can be followed by a filter
          return 'property';
        }
      }

      // Highlight filters
      if (state.waitFilter) {
        state.waitFilter = false;
        if (stream.match(filters)) {
          return 'variable-2';
        }
      }

      // Ignore all white spaces
      if (stream.eatSpace()) {
        state.waitProperty = false;
        return 'null';
      }

      // Identify numbers
      if (stream.match(/\b\d+(\.\d+)?\b/)) {
        return 'number';
      }

      // Identify strings
      if (stream.match("'")) {
        state.tokenize = inString("'", state.tokenize);
        return 'string';
      } else if (stream.match('"')) {
        state.tokenize = inString('"', state.tokenize);
        return 'string';
      }

      // Attempt to match an operator
      if (stream.match(operators)) {
        return 'operator';
      }

      // Attempt to match a word operator
      if (stream.match(wordOperators)) {
        return 'keyword';
      }

      // Attempt to match a keyword
      var keywordMatch = stream.match(keywords);
      if (keywordMatch) {
        if (keywordMatch[0] == 'comment') {
          state.blockCommentTag = true;
        }
        return 'keyword';
      }

      // Attempt to match a variable
      if (stream.match(/\b(\w+)\b/)) {
        state.waitDot = true;
        state.waitPipe = true; // A property can be followed by a filter
        return 'variable';
      }

      // If found closing tag reset
      if (stream.match('%}')) {
        state.waitProperty = null;
        state.waitFilter = null;
        state.waitDot = null;
        state.waitPipe = null;
        // If the tag that closes is a block comment tag, we want to mark the
        // following code as comment, until the tag closes.
        if (state.blockCommentTag) {
          state.blockCommentTag = false; // Release the "lock"
          state.tokenize = inBlockComment;
        } else {
          state.tokenize = tokenBase;
        }
        return 'tag';
      }

      // If nothing was found, advance to the next character
      stream.next();
      return 'null';
    }

    // Mark everything as comment inside the tag and the tag itself.
    function inComment(stream, state) {
      if (stream.match(/^.*?#\}/)) state.tokenize = tokenBase;
      else stream.skipToEnd();
      return 'comment';
    }

    // Mark everything as a comment until the `blockcomment` tag closes.
    function inBlockComment(stream, state) {
      if (stream.match(/\{%\s*endcomment\s*%\}/, false)) {
        state.tokenize = inTag;
        stream.match('{%');
        return 'tag';
      } else {
        stream.next();
        return 'comment';
      }
    }

    return {
      startState: function() {
        return { tokenize: tokenBase };
      },
      token: function(stream, state) {
        return state.tokenize(stream, state);
      },
      blockCommentStart: '{% comment %}',
      blockCommentEnd: '{% endcomment %}',
    };
  });

  CodeMirror.defineMode('django', function(config) {
    var htmlBase = CodeMirror.getMode(config, 'text/html');
    var djangoInner = CodeMirror.getMode(config, 'django:inner');
    return CodeMirror.overlayMode(htmlBase, djangoInner);
  });

  CodeMirror.defineMIME('text/x-django', 'django');
});
